-- mod: [bounty]
-- description: adds a death certificate, to get a bounty rewards.
-- notes: the idea is to get a death certificate, so you can get a reward from
--        an automated machine.
-- author: boxface
-- license: MIT
-- version: 1.0
-- date: 2019-10-20
-- 
-- version: 1.1
-- date: 2022-08-20
--[[
	*DONE: add bounty mark item
		use item, show dialog, enter username, and place bounty on player
		if more than x players place bounty on player then the bounty becomes
		active and the player bones will drop an item proving the player was
		killed and someone took his bones. (captured bones)
	* do not give player head (bounty reward) to same player
	* show a chat message like: "Server: Someone put a price on <player>'s head."
	* may be interesting to add a list to the 'dead proof' of the items
		captured in the bones.
	* may be bounty price needs to expire at some point.
	* trusted players may place a higher bounty level.
	* new (just registered) players may not place bounty.
	** Add reason for capture.
	* must not place bounty on yourself.
	** TODO: if owner take items from his own bones, give no reward for trash bones.
	** Require at least 2 players to activate bounty. (?)
--]]

if not minetest.global_exists("bounty") then
	bounty = {}
	bounty.wanted = {test = true}
	bounty.author = "boxface"
	bounty.version = 1.1
	bounty.date = "2022-08-20"
	bounty.modname = minetest.get_current_modname()
	bounty.modpath = minetest.get_modpath(bounty.modname)
	bounty.storage = minetest.get_mod_storage()
	-- load register file only the first time
	dofile(bounty.modpath .. "/register.lua")
else
	if not bounty.author == "boxface" then
		local err = "[bounty] another mod registered my global variable."
		minetest.log("error", err)
		return false
	end
end

-- code

bounty.reward = {}
bounty.bones = {}

function bounty.chat_send_player(pname, msg)
	local s = minetest.colorize("goldenrod", msg)
	minetest.chat_send_player(pname, s)
end

function bounty.command_list(pname, param)
	local msg = ""
	nwanted = 0

	for k, v in pairs(bounty.wanted) do
		local count = bounty.get_count(k)
		msg = "Wanted: %s, by %d players."
		msg = string.format(msg, k, count)
		bounty.chat_send_player(pname, msg)
		nwanted = nwanted + 1
	end
	if nwanted == 0 then
		bounty.chat_send_player(pname, "Bounty list is empty.")
	else
		bounty.chat_send_player(pname, "End of list.")
	end
	return true
end


function bounty.get_bounty_data(pos)
	local meta = minetest.get_meta(pos)
	local data = {}
	data.pos = {x = pos.x, y = pos.y, z = pos.z}
	data.owner = meta:get_string("owner")
	data.bounty = meta:get_string("bounty")
	data.date = meta:get_string("date")

	return data
end

function bounty.give_reward(data, player)
	assert(type(data)=="table")
	local pname = player:get_player_name()

	-- do not give bounty proof to same player
	if data.bounty == "" or data.bounty == pname then
		minetest.log("action", "[bounty] no bounty to the same player.")
		return false
	end

	-- check if player is wanted
	if (bounty.check_bounty(data.bounty) == 0) then
		-- no bounty on player, no reward
		-- avoid reward to any bones
		return false
	end

	-- check if bones removed and empty inventory
	local meta = minetest.get_meta(data.pos)
	local owner = meta:get_string("owner")
	local inv = meta:get_inventory("main")
	if owner ~= "" or not inv:is_empty("main") then
		-- inventory must be empty
		-- owner removed
		-- and bones node removed
		return false
	end

	-- create bounty reward stack
	local stack = ItemStack("bounty:reward")
	local smeta = stack:get_meta()
	local player_inv = player:get_inventory()
	smeta:set_string("bounty", data.bounty)
	smeta:set_string("date", data.date)
	smeta:set_string("capturedby", pname)
	smeta:set_string("tombstone", "")
	smeta:set_string("infotext", data.bounty .. "\n dead proof")
	smeta:set_string("description", data.bounty .. " dead proof")
	if player_inv:room_for_item("main", stack) then
		player_inv:add_item("main", stack)
		bounty.chat_send_player(pname, "[bounty] Reward added to your inventory")
	else
		minetest.add_item(data.pos, stack)
		bounty.chat_send_player(pname, "[bounty] Your inventory is full, reward dropped.")
	end
	return true
end

function bounty.mark_use(itemstack, user, pointed_thing)
	-- mark a player as wanted
	-- put price on a player's head

	local pname = user:get_player_name()

	local formspec = bounty.get_mark_formspec()
	minetest.show_formspec(pname, "bounty:mark", formspec)
end


function bounty.place_bounty(target, placer)
	-- set bounty on player
	-- record must contain:
	--
	-- playername: who set bounty.
	-- targetname: wanted player name.
	-- dateadded: date the bounty was placed.
	-- expiry time?
	local pname = placer:get_player_name()
	local has_bounty = bounty.wanted[target] ~= nil
	local result = false

	if not has_bounty then
		-- place first bounty
		local data = {}
		data.placers = {}
		data.placers[pname] = {
				date = os.date("%Y-%m-%d %H:%M")
			}
		bounty.wanted[target] = data
		result = true
	else
		-- check if already placed
		if bounty.wanted[target] and bounty.wanted[target].placers and
			not bounty.wanted[target].placers[pname] then
			-- place bounty if not paced on this target
			bounty.wanted[target].placers[pname] = {
					date = os.date("%Y-%m-%d %H:%H")
				}
			result = true
		end
	end
	if result then
		local msg = "You have placed bounty on <%s>."
		msg = string.format(msg, target)
		bounty.chat_send_player(pname, msg)
	end
	return result
end

function bounty.get_count(target)
	local count = 0
	if bounty.wanted[target] and bounty.wanted[target].placers then
		for k, v in pairs(bounty.wanted[target].placers) do
			count = count + 1
		end
	end
	return count
end

function bounty.remove_bounty(target, placer)
	local pname = placer:get_player_name()
	local result = false
	if bounty.wanted[target] then
		if bounty.wanted[target].placers and
				bounty.wanted[target].placers[pname] then
			bounty.wanted[target].placers[pname] = nil
			local msg = "You removed bounty from <%s>."
			msg = string.format(msg, target)
			bounty.chat_send_player(pname, msg)
			result = true
			-- if list is empty, clear record
			if bounty.get_count(target) == 0 then
				bounty.wanted[target] = nil
				local msg = "Bounty on %s has been cleared."
				msg = string.format(msg, target)
				bounty.chat_send_player(pname, msg)
			end
		end
	end
	return result
end

function bounty.check_bounty(target)
	local data = bounty.wanted[target]
	print("Dump " .. target .. ": " .. dump(data))
	local count = 0
	if data and data.placers then
		for k, v in pairs(data.placers) do
			count = count + 1
		end
	end

	print("Count: " .. count)
	return count
end

-- test
function bounty.do_dieplayer(objref, reason)
	local deadplayer = objref:get_player_name()
	local wanted_count = bounty.check_bounty(deadplayer)
	local msg = "Player <%s> died, wanted level %d."
	msg = string.format(msg, deadplayer, wanted_count)
	msg = minetest.colorize("cyan", msg)
	minetest.chat_send_all(msg)
	if wanted_count > 0 then
		local msg = "Player <%s> is wanted by %d players."
		msg = string.format(msg, deadplayer, wanted_count)
		msg = minetest.colorize("cyan", msg)
		minetest.chat_send_all(msg)
	end
	if reason and reason.object and reason.object:is_player() then
		if wanted_count > 0 then
			local pname = reason.object:get_player_name()
			local msg = "Server: Player <%s> has a price on his head."
			msg = string.format(msg, deadplayer)
			msg = minetest.colorize("darkorange", msg)
			minetest.chat_send_player(pname, msg)
		end
	end
end

function bounty.get_mark_formspec()
	local formspec = {
			"formspec_version[2]",
			"size[8,4]",
			"box[0.25,0.25;7.5,1;#80C0C0]",
			"item_image[0.4,0.4;0.8,0.8;bounty:mark]",
			"label[1.5,0.5;Bounty marker]",
			"label[1.5,1.0;Put a price on a player's head.]",
		--	"label[1.5,1.5;Get their bones and get a proof.]",
			"field[0.5,2;7,0.8;bountytarget;Player name:;]",
			"field_close_on_enter[bountytarget;false]",
			"style[place;bgcolor=#E00000]",
			"style[remove;bgcolor=#00E000]",
			"tooltip[place;Place bounty.]",
			"tooltip[remove;Remove bounty.]",
			"button_exit[0.5,3;1.75,0.8;place;Place]",
			"button_exit[2.5,3;1.75,0.8;remove;Remove]",
			"button_exit[4.5,3;1.75,0.8;cancel;Cancel]",
			-- debug
			--"style[dump;bgcolor=#0000F0]",
			--"button_exit[6.5,3;1,0.8;dump;Test]"
		}
	local str = table.concat(formspec)
	-- str = string.gsub(str, "${variable}", variable)
	return str
end

function bounty.get_tombstone_formspec(data, readonly)
	if data == nil then
		data = {}
	end
	local bountyname = data.bounty or "unknown"
	local capturedby = data.capturedby or "unknown"
	local captureddate = data.date or ""
	local tombstone = data.tombstone or ""

	local formspec = {
			"formspec_version[2]",
			"size[6,5]",
			"box[0.25,0.25;5.5,1;#80C0C0]",
			"item_image[0.4,0.4;0.8,0.8;bounty:reward]",
			"label[1.5,0.5;Captured bones]",
			"label[1.5,1.0;${bountyname}]",
			"label[0.25,1.5;Captured by: ${capturedby}]",
			"label[0.25,2.0;Date: ${date}]",
			"field[0.25,3.0;5.5,0.8;tombstone;Tombstone text;${tombstone}]",
			"button_exit[1.5,4;3,0.8;write;Write]"
		}
	
	if readonly then
		formspec = {
			"formspec_version[2]",
			"size[6,5]",
			"box[0.25,0.25;5.5,1;#80C0C0]",
			"item_image[0.4,0.4;0.8,0.8;bounty:reward]",
			"label[1.5,0.5;Captured bones]",
			"label[1.5,1.0;${bountyname}]",
			"label[0.25,1.5;Captured by: ${capturedby}]",
			"label[0.25,2.0;Date: ${date}]",
			"label[0.25,3.0;${tombstone}]",
			"button_exit[1.5,4;3,0.8;close;Close]"
		}

	end

	str = table.concat(formspec)
	str = string.gsub(str, "${bountyname}", bountyname)
	str = string.gsub(str, "${date}", captureddate)
	str = string.gsub(str, "${capturedby}", capturedby)
	str = string.gsub(str, "${tombstone}", tombstone)
	return str
end

function bounty.reward.do_after_place_node(pos, placer, itemstack, pointed_thing)
	-- bounty reward
	local meta = minetest.get_meta(pos)
	local imeta = itemstack:get_meta()
	meta:set_string("bounty", imeta:get_string("bounty"))
	meta:set_string("date", imeta:get_string("date"))
	meta:set_string("capturedby", imeta:get_string("capturedby"))
	meta:set_string("tombstone", imeta:get_string("tombstone"))
	meta:set_string("infotext", imeta:get_string("bounty") ..
			"\nDate: " .. imeta:get_string("date") ..
			"\nCaptured by: " .. imeta:get_string("capturedby") ..
			"\n" .. imeta:get_string("tombstone"))
	meta:set_string("description", imeta:get_string("description"))
	return false
end

function bounty.reward.do_preserve_metadata(pos, oldnode, oldmeta, drops)
	-- bounty reward
	-- debug
	--minetest.chat_send_all(
	--		minetest.colorize("darkgrey", "Debug: do_preserve_metadata"))
	print("preserve_metadata:")
	print(dump(drops))
	print("oldmeta:")
	print(dump(oldmeta))
	stack = drops[1]
	smeta = stack:get_meta()
	smeta:set_string("bounty", oldmeta["bounty"])
	smeta:set_string("date", oldmeta["date"])
	smeta:set_string("capturedby", oldmeta["capturedby"])
	smeta:set_string("tombstone", oldmeta["tombstone"])
	smeta:set_string("infotext", oldmeta["infotext"])
	smeta:set_string("description", oldmeta["description"])
end

function bounty.reward.do_use(itemstack, user, pointed_thing)
	-- bounty reward
	local pname = user:get_player_name()
	local imeta = itemstack:get_meta()
	--local bountyname = imeta:get_string("bounty")
	--local tombstone = imeta:get_string("tombstone")
	-- minetest.chat_send_player(pname, "USING THIS THING!")
	local data = {
		bounty = imeta:get_string("bounty"),
		tombstone = imeta:get_string("tombstone"),
		date = imeta:get_string("date"),
		capturedby = imeta:get_string("capturedby")
	}
	local formspec = bounty.get_tombstone_formspec(data)
	minetest.show_formspec(pname, "bounty:tombstone", formspec)
--	minetest.show_formspec(pname, "bounty:tombstone",
--			bounty.get_tombstone_formspec(bountyname, tombstone))
end

function bounty.reward.do_rightclick(pos, node, clicker, pointed_thing)
	local pname = clicker:get_player_name()
	local meta = minetest.get_meta(pos)
	local data = {
		bounty = meta:get_string("bounty"),
		tombstone = meta:get_string("tombstone"),
		date = meta:get_string("date"),
		capturedby = meta:get_string("capturedby")
	}
	local formspec = bounty.get_tombstone_formspec(data, true)
	minetest.show_formspec(pname, "bounty:tombstonerw", formspec)
end

function bounty.do_player_receive_fields(player, formname, fields)

	if formname == "bounty:mark" then
		-- debug
		--local dbg = "formspec[bounty:mark]\nFields: " .. dump(fields)
		--dbg = minetest.colorize("magenta", dbg)
		--minetest.chat_send_player(player:get_player_name(), dbg)

		-- 
		local bountylevel = 0
		if fields.key_enter_field == "bountytarget" then
			if fields.bountytarget ~= "" then
				-- bounty.check_bounty(fields.bountytarget)
			end
		end

		-- check buttons
		if fields.dump then
			minetest.chat_send_player(player:get_player_name(),
					"Wanted list: " .. dump(bounty.wanted))
		elseif fields.place then
			local pname = player:get_player_name()
			local target = fields.bountytarget
			
			-- player exists?
			--if bountytarget and minetest.builtin_auth_handler.get_auth(bountytarget) ~= nil then
			--	minetest.chat_send_player(pname, "Player found!")
			--else
			--	minetest.chat_send_player(pname, "That player is not found.")
			--end
			
			if target ~= "" then
				-- place bounty
				if bounty.place_bounty(target, player) then
					-- use bounty tag
					wield = player:get_wielded_item()
					if wield:get_name() == "bounty:mark" then
						wield:take_item()
						player:set_wielded_item(wield)
					end
				end
--				bounty.storage.
			end
		elseif fields.remove then
			local pname = player:get_player_name()
			local target = fields.bountytarget
			if target ~= "" then
				if bounty.remove_bounty(target, player) then
					-- DONE: use bounty tag
					wield = player:get_wielded_item()
					if wield:get_name() == "bounty:mark" then
						wield:take_item()
						player:set_wielded_item(wield)
					end
				end
			end
		end
	elseif formname == "bounty:tombstone" then
		if fields.tombstone then
			-- update tombstone text
			local pname = player:get_player_name()
			local wielditem = player:get_wielded_item()
			if not wielditem then
				return
			end
			-- debug
			print("Setting wielded item " .. fields.tombstone)
			local wieldmeta = wielditem:get_meta()
			wieldmeta:set_string("tombstone", fields.tombstone)
			player:set_wielded_item(wielditem)
		end
	end
end

function bounty.do_shutdown()
	bounty.store_save()
end

function bounty.store_load()
	bounty.wanted = minetest.deserialize(bounty.storage:get_string("wanted")) or {}
end

function bounty.store_save()
	bounty.storage:set_string("wanted", minetest.serialize(bounty.wanted))
end

-- ========== BONES OVERRIDE FUNCTIONS ==========
function bounty.override_bones()
	local bones = minetest.registered_nodes["bones:bones"]

	bounty.oldbones_metadata_inventory_take = bones.on_metadata_inventory_take
	bones.on_metadata_inventory_take = function(...)
			return bounty.bones.do_metadata_inventory_take(...)
		end

	bounty.oldbones_on_punch = bones.on_punch
	bones.on_punch = function(...)
			return bounty.bones.do_punch(...)
		end

	bounty.oldbones_on_timer = bones.on_timer
	bones.on_timer = function(...)
			return bounty.bones.do_timer(...)
		end
end

function bounty.bones.do_metadata_inventory_take(pos, listname, index, stack, player)
	-- get bounty data
	local bountydata = bounty.get_bounty_data(pos)

	-- original bones callback
	local result = bounty.oldbones_metadata_inventory_take(pos, listname, index, stack, player)

	-- check if node was removed
	-- give bounty reward if applicable
	bounty.give_reward(bountydata, player)

	return result
end

function bounty.bones.do_punch(pos, node, player)
	-- get bounty data
	local bountydata = bounty.get_bounty_data(pos)

	-- original callback
	local result = bounty.oldbones_on_punch(pos, node, player)

	-- check if node was removed
	-- give bounty reward if applicable
	bounty.give_reward(bountydata, player)

	return result
end

function bounty.bones.do_timer(pos, elapsed)
	local meta = minetest.get_meta(pos)
	
	local owner = meta:get_string("owner")
	if owner ~= "" and  bounty.get_count(owner)>0 then
		-- add "bounty" name to bones
		meta:set_string("bounty", meta:get_string("owner"))
		-- add "deathdate" to bones
		local deathdate = os.date("%Y-%m-%d %H:%M")
		meta:set_string("date", deathdate)
	end
	-- bones callback function
	local result = bounty.oldbones_on_timer(pos, elapsed)
	
	-- debug
	--local time = meta:get_int("time") + elapsed
	--if result then
		-- minetest.chat_send_all(minetest.colorize("orange", 
		--	"Server: player bones still warm. (time: ".. time .. ")."))
	--else
		-- minetest.chat_send_all(minetest.colorize("cyan",
		--	"Server: player bones got cold!"))
	--end
	return result
end

-- ---------- END BONES OVERRIDE FUNCTIONS ----------

-- initialization
if not bounty.runonce then
	-- debug
	local share_bones_time = tonumber(minetest.settings:get("share_bones_time")) or 1200
	print("Debug: Share bones time: " .. share_bones_time)

	minetest.register_chatcommand("bounty", {
			params = "",
			description = "Shows bounty list.",
			privs = {},
			func = function(...)
					return bounty.command_list(...)
				end
			})

	minetest.register_on_dieplayer(function(...)
				bounty.do_dieplayer(...)
			end)
	minetest.register_on_player_receive_fields(function(...)
				bounty.do_player_receive_fields(...)
			end)
	minetest.register_on_shutdown(function(...)
				return bounty.do_shutdown(...)
			end)

	bounty.override_bones()
	bounty.store_load()

	if minetest.get_modpath("reload") then
		reload.register_file("bounty:init", bounty.modpath .. "/init.lua", false)
	end

	bounty.runonce = true
end

--<eof>
